
# vocab.py
# this is the class for a collection of flash cards

from PyQt4 import QtGui, QtXml, QtCore
# QXmlStreamWriter might be in QtXml or QtCore so import all from both and don't specify module
from PyQt4.QtXml import *
from PyQt4.QtCore import *
import random, pdb, codecs, csv
from constants import flashblack_version
from flashcard import *
from activecarddialog import *
from editcarddialog import *
from wordtypedialog import *
from editfontsdialog import *
from configdialog import *
from wordtype import *
from unicodecsv import *
from user import home


class Vocab:

    def __init__(self, parent=None):
        self.mainGui = parent
        
        self.filename = ''
        
        self.fonts = {}
        
        self.changed = False
        
        self.difficultyInc = 3
        self.difficultyDec = -1
        
        self.defaultCardValues = {'chapter': '1', 'difficulty': '50', 'foreign': 'Phrase', 'english': 'Translation'}

        self.wordtypes = {}
        
        self.cards = []
        self.activeCards = QtGui.QStackedWidget(self.mainGui)
        self.progress = QtGui.QProgressBar(self.mainGui)
        self.progress.hide()
    
        self.dialog = None # just to keep a reference to the most recently active dialog
        
        self.activeCardFilter = None
        # create a singleton reference (per vocab) so we don't have to keep rebuiling this dialog and it remembers its state
        self.activeCardDialogSingleton = None
    
    def unset(self):
        self.filename = ''
        del self.fonts
        del self.wordtypes
        if self.activeCards.count() > 0:
            # hide current card, otherwise it still shows after we've removed it
            self.activeCards.currentWidget().reset()
            self.activeCards.currentWidget().show(False, FieldShow.ALWAYS)
            for card in self.cards:
                if self.isActive(card):
                    self.activeCards.removeWidget(card)
        del self.cards
        self.progress.hide()
        self.activeCardDialogSingleton = None
        self.fonts = {}
        self.wordtypes = {}
        self.cards = []
    
    def newVocab(self):
        if self.changed:
            reply = QtGui.QMessageBox.warning(self.mainGui, 'Vocab has been changed',
                "Would you like to save your changes to the vocab?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No, QtGui.QMessageBox.Cancel)
            if reply == QtGui.QMessageBox.Yes:
                self.saveVocab()
            elif reply == QtGui.QMessageBox.No:
                pass
            else:
                return
        self.unset()
        
        self.activeCardFilter = CardFilter({}, {}, [], True) # all cards active to begin with
        
        # provide 2 fonts to start with
        self.fonts['standard'] = QtGui.QFont()
        self.fonts['big'] = QtGui.QFont()
        self.fonts['big'].setPointSize(30)
        # provide a noun wordtype to start with
        fields = []
        fields.append(Field(self, 'chapter', 'Chapter', 'standard', FieldPosition.LEFT, FieldShow.ALWAYS))
        fields.append(Field(self, 'difficulty', 'Difficulty', 'standard', FieldPosition.RIGHT, FieldShow.ALWAYS))
        fields.append(Field(self, 'foreign', 'Foreign word', 'big', FieldPosition.CENTRE, FieldShow.ALWAYS))
        fields.append(Field(self, 'english', 'Translation', 'big', FieldPosition.CENTRE, FieldShow.ONREVEAL))
        fields.append(Field(self, 'hint', 'Hint', 'standard', FieldPosition.CENTRE, FieldShow.ONHINT))
        self.wordtypes['noun'] = WordType(self, 'noun', fields)
        self.mainGui.setVocabActionsEnabled(True)
        self.mainGui.setCardActionsEnabled(False)
        self.mainGui.setButtonsEnabled(False)
        QtGui.QMessageBox.information(self.mainGui, 'New Vocab',
                'A new Vocab has been created. It does not yet contain any cards.')
    
    def setActiveCards(self):
        if self.activeCardFilter == None:
            self.selectActiveCards()
            return
        # get rid of current active cards
        self.mainGui.everythingLayout.removeWidget(self.activeCards)
        self.activeCards.hide()
        self.activeCards = QtGui.QStackedWidget(self.mainGui)
        self.mainGui.everythingLayout.insertWidget(1, self.activeCards)
        for card in self.cards:
            self.checkFilter(card)
        if self.activeCards.count():
            self.progress.show()
            self.progress.setRange(0, self.activeCards.count())
            self.progress.setValue(1)
            self.mainGui.setButtonsEnabled(True)
            self.mainGui.previousButton.setEnabled(False)
            self.mainGui.setCardActionsEnabled(True)
        else:
            QtGui.QMessageBox.information(self.mainGui, 'No Active Cards',
                'No cards are in that range')
            self.mainGui.setButtonsEnabled(False)
            self.mainGui.setCardActionsEnabled(False)
            self.progress.hide()
        
    def checkFilter(self, card, index = None):
        if not self.activeCardFilter == None:
            if self.activeCardFilter.filterCard(card):
                if not self.isActive(card):
                    card.doLayout()
                    if index == None:
                        self.activeCards.addWidget(card)
                    else:
                        self.activeCards.insertWidget(index, card)
            else:
                if self.isActive(card):
                    self.activeCards.removeWidget(card)
    
    def isActive(self, card):
        if self.activeCards.count() == 0:
            return False
        for i in range(0, self.activeCards.count()):
            if card == self.activeCards.widget(i):
                return True
        return False
    
    def loadVocab(self, parent = None):
        """Load a vocab from a file."""
        filter = "XML files (*.xml);;CSV files (*.csv);;All files (*)"
        loadFile = ''
        if self.filename == '':
            loadFile = QtGui.QFileDialog.getOpenFileName(self.mainGui, 'Open Vocab', home, filter)
        else:
            loadFile = QtGui.QFileDialog.getOpenFileName(self.mainGui, 'Open Vocab', self.filename, filter)
        if not (loadFile == ''):
            if self.changed:
                reply = QtGui.QMessageBox.warning(self.mainGui, 'Vocab has been changed',
                    "Would you like to save your changes to the vocab?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No, QtGui.QMessageBox.Cancel)
                if reply == QtGui.QMessageBox.Yes:
                    self.saveVocab()
                elif reply == QtGui.QMessageBox.No:
                    pass
                else:
                    return
            self.unset()
            self.filename = loadFile
            if self.filename.toLower().endsWith('.csv'):
                self.loadVocabCSV(parent)
            else:
                self.loadVocabXML()
            self.changed = False
            self.mainGui.setVocabActionsEnabled(True)
            self.selectActiveCards()
    
    def loadVocabCSV(self, parent):
        try:
            csvfile = codecs.open(self.filename, encoding='utf-8', mode='r')
            #file = open(self.filename, 'r')
            firstLine = csvfile.readline()
            csvReader = None
            if firstLine.startswith('"flashblack') or firstLine.startswith('flashblack'):
                # we are delaing with version 3.0 or later. Default dialect is used.
                csvReader = unicode_csv_reader(csvfile)
            else:
              try:
                csv.Sniffer().sniff(csvfile.read(1024))
                # if we didn't get an exception that means there's quotes around the strings
                # we can't use the dialect that the sniffer returns because it produces a strane and inexplicable error
                csvReader = unicode_csv_reader(csvfile, delimiter=';', quotechar='"', skipinitialspace=True)
                csvfile.seek(0)
              except csv.Error, e:
                # however if the sniffer does raise an execption, it means there are no quotes
                csvReader = unicode_csv_reader(csvfile, delimiter=';', quoting=csv.QUOTE_NONE, skipinitialspace=True)
                csvfile.seek(0)
            rows = []
            try:
                self.difficultyInc = float(csvReader.next()[0])
                self.difficultyDec = float(csvReader.next()[0])
                for row in csvReader:
                    rows.append(row)
            except csv.Error, e:
                sys.exit('file %s, line %d: %s' % (filename, reader.line_num, e))

            csvfile.close()
            self.readFontsCSV(rows)
            self.readWordTypesCSV(rows)
            self.cards = []
            for row in rows:
                try:
                    self.cards.append(flashCardFromCSV(self, parent, row))
                except FileFormatError, e:
                    print e.message
        except IOError, e:
            QtGui.QMessageBox.critical(self.mainGui, "Unable to Load", "Could not load the vocab file.\n" + e.__repr__())

    def readFontsCSV(self, input):
        if not input[0][0].startswith('font data'):
            return
        self.fonts = {}
        input.pop(0)
        while not input[0][0].startswith('/font data'):
            font = QtGui.QFont()
            font.fromString(input[0][1])
            self.fonts[input[0][0]] = font
            input.pop(0)
        input.pop(0) # '/font data'

    def readWordTypesCSV(self, input):
        if not input[0][0].startswith('word-type data'):
            return
        self.wordtypes = {}
        input.pop(0)
        while not input[0][0].startswith('/word-type data'):
            typeName = input[0][0]
            self.wordtypes[typeName] = WordType(self, typeName, list())
            input.pop(0)
            while not input[0][0].startswith('/' + typeName):
                #print typeName
                self.wordtypes[typeName].fields.append(Field(self, input[0][0], input[0][1], input[0][2], int(input[0][3]), int(input[0][4])))
                input.pop(0)
            input.pop(0) # '/typename'
        input.pop(0) # '/word-type data'
        # print len(self.wordtypes['noun'].fields)

    def loadVocabXML(self):
        try:
            xmlFile = QtCore.QFile(self.filename)
            xmlFile.open(QtCore.QIODevice.ReadOnly)
            xmlFileRead = QtXml.QXmlInputSource(xmlFile)
            xmlReader = QtXml.QXmlSimpleReader()
            xmlHandler = VocabXmlHandler(self)
            xmlReader.setContentHandler(xmlHandler)
            xmlReader.setErrorHandler(xmlHandler)
            xmlReader.parse(xmlFileRead)
                
        except IOError, e:
            QtGui.QMessageBox.critical(self.mainGui, "Unable to Load", "Could not load the vocab file.\n" + e.__repr__())
    
    def saveVocab(self):
        if self.filename == '':
            self.saveVocabAs()
        else:
            if self.filename.toLower().endsWith('.csv'):
                self.saveVocabCSV()
            elif self.filename.toLower().endsWith('.xml'):
                self.saveVocabXML()
            else:
                self.filename = self.filename + '.xml'
                self.saveVocabXML()

    def saveVocabAs(self):
        """Save this vocab into a file.
        
        Creates a xml or csv file storing details of the wordtypes and the flashcards contained by this vocab."""
        filter = "XML file (*.xml);;CSV file (*.csv)"
        if self.filename == '':
            self.filename = QtGui.QFileDialog.getSaveFileName(self.mainGui, 'Save Vocab', home, filter)
        else:
            self.filename = QtGui.QFileDialog.getSaveFileName(self.mainGui, 'Save Vocab', self.filename, filter)
        if self.filename != '':
            self.saveVocab()
    
    def saveVocabCSV(self):
        try:
            outFile = codecs.open(self.filename, mode='wb')
            #outFile = codecs.open(self.filename, encoding='utf-8', mode='wb')
            csvWriter = UnicodeWriter(outFile)
            csvWriter.writerow([flashblack_version])
            csvWriter.writerow([str(self.difficultyInc)])
            csvWriter.writerow([str(self.difficultyDec)])
            # output font data
            csvWriter.writerow(['font data'])
            for key in self.fonts:
                csvWriter.writerow([key, str(self.fonts[key].toString())])
            csvWriter.writerow(['/font data'])
            # write out the word type data
            csvWriter.writerow(['word-type data'])
            for key in self.wordtypes:                
                self.wordtypes[key].toCSV(csvWriter)
            csvWriter.writerow(['/word-type data'])
            # then write out the card data
            for card in self.cards:
                card.toCSV(csvWriter)
            outFile.close()
            self.changed = False
        except IOError, e:
            QtGui.QMessageBox.critical(self.mainGui, "Unable to Save", "Could not save the vocab file.\n" + e.__repr__())
    
    def saveVocabXML(self):
        try:
            xmlFile = QtCore.QFile(self.filename)
            xmlFile.open(QtCore.QIODevice.WriteOnly)
            xmlWriter = QXmlStreamWriter(xmlFile)
            xmlWriter.setAutoFormatting(True)
            xmlWriter.writeStartDocument()
            xmlWriter.writeStartElement('vocab')
            #xmlWriter.writeTextElement('languageName', self.languageName)
            xmlWriter.writeTextElement('difficulty_increment', str(self.difficultyInc))
            xmlWriter.writeTextElement('difficulty_decrement', str(self.difficultyDec))
            xmlWriter.writeStartElement('fonts')
            for key in self.fonts:
                xmlWriter.writeStartElement('font')
                xmlWriter.writeAttribute('name', key)
                xmlWriter.writeCharacters(self.fonts[key].toString())
                xmlWriter.writeEndElement()
            xmlWriter.writeEndElement()
            xmlWriter.writeStartElement('wordTypes')
            for key in self.wordtypes:                
                self.wordtypes[key].writeXML(xmlWriter)
            xmlWriter.writeEndElement()
            xmlWriter.writeStartElement('flashCards')
            for card in self.cards:
                card.writeXML(xmlWriter)
            xmlWriter.writeEndElement()
            xmlWriter.writeEndElement()
            xmlWriter.writeEndDocument()
            self.changed = False
        except IOError, e:
            QtGui.QMessageBox.critical(self.mainGui, "Unable to Save", "Could not save the vocab file.\n" + e.__repr__())
    
    def editWordTypes(self):
        self.dialog = WordTypeDialog(self, self.mainGui)
        self.dialog.show()
    
    def editFonts(self):
        self.dialog = EditFontsDialog(self, self.mainGui)
        self.dialog.show()
    
    def editDifficultyChange(self):
        self.dialog = ConfigDialog(self, self.mainGui)
        self.dialog.show()
    
    def revealCurrentCard(self):
        self.activeCards.currentWidget().show(True, FieldShow.ONREVEAL)
        self.mainGui.revealButton.setEnabled(False)
        # also reavel hint
        self.revealCurrentCardHint()
    
    def revealCurrentCardHint(self):
        self.activeCards.currentWidget().show(True, FieldShow.ONHINT)
        self.mainGui.hintButton.setEnabled(False)
    
    def nextCard(self):
        self.resetCurrentCard()
        self.activeCards.setCurrentIndex(self.activeCards.currentIndex() + 1)
        self.progress.setValue(self.progress.value() + 1)
        self.mainGui.previousButton.setEnabled(True)
        if self.progress.value() >= self.progress.maximum():
            self.mainGui.nextButton.setEnabled(False)
    
    def previousCard(self):
        self.resetCurrentCard()
        self.activeCards.setCurrentIndex(self.activeCards.currentIndex() - 1)
        self.progress.setValue(self.progress.value() - 1)
        self.mainGui.nextButton.setEnabled(True)
        if self.progress.value() <= 1:
            self.mainGui.previousButton.setEnabled(False)
    
    def resetCurrentCard(self):
        self.activeCards.currentWidget().reset()
        self.mainGui.revealButton.setEnabled(True)
        self.mainGui.hintButton.setEnabled(True)
    
    def gotItRight(self):
        self.activeCards.currentWidget().changeDifficulty(self.difficultyDec)
        self.nextCard()
    
    def gotItWrong(self):
        self.activeCards.currentWidget().changeDifficulty(self.difficultyInc)
        self.nextCard()
    
    def selectActiveCards(self):
        if self.activeCardDialogSingleton == None:
            self.activeCardDialogSingleton = ActiveCardDialog(self, self.mainGui)
        self.dialog = self.activeCardDialogSingleton
        self.dialog.show()
    
    def shuffleActiveCards(self):
        self.resetCurrentCard()
        cards = []
        self.progress.setValue(1)
        for i in range(0, self.activeCards.count()-1):
            cards.append(self.activeCards.currentWidget())
            self.activeCards.removeWidget(cards[i])
        random.shuffle(cards)
        for card in cards:
            self.activeCards.addWidget(card)
    
    def editCurrentCard(self):
        card = self.activeCards.currentWidget()
        self.dialog = EditCardDialog(card, self.mainGui)
        self.dialog.show()
    
    def newCard(self, card = None):
        if (card == None and self.activeCards.count()):
            card = self.activeCards.currentWidget()
        self.dialog = NewCardDialog(self, card, self.mainGui)
        self.dialog.show()
    
    def cardChanged(self, card):
        if self.isActive(card):
            activeIndex = self.activeCards.indexOf(card)
            self.checkFilter(card, activeIndex)
        else:
            self.checkFilter(card)
        if self.isActive(card):
            self.activeCards.setCurrentWidget(card)
            self.resetCurrentCard()
            self.mainGui.setButtonsEnabled(True)
            self.progress.show()
            self.progress.setMaximum(self.activeCards.count())
            if self.progress.value() <= 1:
                self.mainGui.previousButton.setEnabled(False)
            if self.progress.value() >= self.progress.maximum():
                self.mainGui.nextButton.setEnabled(False)
    
    def deleteCurrentCard(self):
        card = self.activeCards.currentWidget()
        self.activeCards.removeWidget(card)
        self.progress.setMaximum(self.activeCards.count())
        self.cards.remove(card)
    
    def replaceCard(self, card, replacement):
        activeIndex = None
        if self.isActive(card):
            activeIndex = self.activeCards.indexOf(card)
            self.activeCards.removeWidget(card)
        i = self.cards.index(card)
        self.cards.remove(card)
        self.cards.insert(i, replacement)
        self.checkFilter(replacement, activeIndex)
    
    def duplicateCard(self, card):
        dup = card.copy()
        self.cards.insert(self.activeCards.currentIndex(), dup)
        if self.isActive(card):
            activeIndex = self.activeCards.indexOf(card)
            self.activeCards.insertWidget(activeIndex + 1, dup)
            dup.doLayout()
            self.progress.setRange(0, self.progress.maximum() + 1)
        self.changed = True
    
    def duplicateCurrentCard(self):
        self.duplicateCard(self.activeCards.currentWidget())
    
    def refreshAllCards(self):
        # for each card that has been laid out replace it with a copy of itself so it redoes the layout.
        for card in self.cards:
            if len(card.guiArea) > 0:
                newCard = card.copy()
                if self.isActive(card):
                    self.activeCards.removeWidget(card)
                    card.setParent(None)
                i = self.cards.index(card)
                self.cards.remove(card)
                self.cards.insert(i, newCard)
        self.setActiveCards()
    
    def refreshFonts(self):
        for card in self.cards:
            card.refreshFonts()
        

class VocabXmlHandler(QtXml.QXmlDefaultHandler):
    
    def __init__(self, vocab):
        QtXml.QXmlDefaultHandler.__init__(self)
        self.vocab = vocab
    
    def startDocument(self):
        self.elementStack = []
        self.fontName = ''
        self.wordType = ''
        self.fieldName = ''
        self.cardType = ''
        self.justStarted = '' # this helps us know when we parse an element with no content
        return True

    def startElement(self, namespaceURI, localName, qName, attributes):
        self.elementStack.append(str(localName))
        self.justStarted = str(localName)
        if localName == 'font':
            self.fontName = str(attributes.value('name'))
        if localName == 'wordType':
            self.wordType = str(attributes.value('name'))
            self.wordTypeData = []
        if localName == 'field':
            self.fieldName = str(attributes.value('name'))
            self.fieldData = {}
        if localName == 'flashCard':
            self.cardType = str(attributes.value('type'))
            self.cardData = {}
        return True
    
    def endElement(self, namespaceURI, localName, qName):
        if self.justStarted == str(localName):
            # we have an element with no content. Need to call the characters method with an empty string
            self.characters('')
        self.justStarted = ''
        self.elementStack.pop()
        if localName == 'field':
            # FIXME: should probably check that all the values for a field have been collected
            self.wordTypeData.append(Field(self.vocab, self.fieldName, self.fieldData['label'], self.fieldData['font'], self.fieldData['position'], self.fieldData['show']))
        elif localName == 'wordType':
            self.vocab.wordtypes[self.wordType] = WordType(self.vocab, self.wordType, self.wordTypeData)
        elif localName == 'flashCard':
            self.vocab.cards.append(FlashCard(self.vocab, self.vocab.mainGui, self.cardType, self.cardData))
        return True
    
    def characters(self, content):
        self.justStarted = ''
        if self.elementStack[len(self.elementStack) - 2] == 'vocab':
            #if self.elementStack[len(self.elementStack) - 1] == 'languageName':
            #    self.vocab.languageName = str(content)
            if self.elementStack[len(self.elementStack) - 1] == 'difficulty_increment':
                self.vocab.difficultyInc = float(content)
            if self.elementStack[len(self.elementStack) - 1] == 'difficulty_decrement':
                self.vocab.difficultyDec = float(content)
        elif self.elementStack[len(self.elementStack) - 2] == 'fonts':
            font = QtGui.QFont()
            font.fromString(content)
            #FIXME: should do something sensible if font is not recognised
            self.vocab.fonts[self.fontName] = font
        elif self.elementStack[len(self.elementStack) - 2] == 'field':
            self.fieldData[self.elementStack[len(self.elementStack) - 1]] = unicode(content)
        elif self.elementStack[len(self.elementStack) - 2] == 'flashCard':
            self.cardData[self.elementStack[len(self.elementStack) - 1]] = unicode(content)
        return True
